(function() {

	/**
	 * TestSuite
	 */
	function TestSuite() {
		this.testCases = arguments;
	}

	TestSuite.prototype.run = function() {
		for ( var i = 0; i < this.testCases.length; i++) {
			var element = this.testCases[i];
			var tests;

			if (element instanceof Array) {
				tests = element;
			} else {
				tests = [ element ];
			}

			for ( var j = 0; j < tests.length; j++) {
				new TestRunner(tests[j]).run();
			}
		}
	};

	/**
	 * Test Runner
	 */
	function TestRunner(testCase, testRenderer) {
		if (testCase != null) {
			this.init(testCase, testRenderer);
		}
	}

	TestRunner.prototype.init = function(testCase, testRenderer) {
		this.testCase = testCase;
		this.compiledTest = null;
		this.testRenderer = (testRenderer != null) ? testRenderer : new DefaultTestRenderer();
	};

	TestRunner.prototype.run = function() {
		this.onStartTests();

		try {
			this.callSetUpBeforeClass();
		} catch (error) {
			alert("Error while executing setUpBeforeClass(): " + error);
			throw error;
		}

		this.callTests();

		try {
			this.callTearDownAfterClass();
		} catch (error) {
			alert("Error while executing tearDownAfterClass(): " + error);
			throw error;
		}

	};

	TestRunner.prototype.callSetUpBeforeClass = function() {
		this.getCompiledTest().getSetUpBeforeClass()();
	};

	TestRunner.prototype.callTearDownAfterClass = function() {
		this.getCompiledTest().getTearDownAfterClass()();
	};

	TestRunner.prototype.callTests = function() {
		for ( var i = 0; i < this.getCompiledTest().getTests().length; i++) {
			try {
				this.callTest(i);
			} catch (error) {
				if (error instanceof AssertException) {
					this.onAssertException(error, this.getCompiledTest().getTests()[i]);
				} else {
					this.onTestError(error, this.getCompiledTest().getTests()[i]);
				}
			}
		}
	};

	TestRunner.prototype.callTest = function(i) {
		this.onCallTest(this.getCompiledTest().getTests()[i]);

		this.getCompiledTest().getSetUp()();
		this.getCompiledTest().getTests()[i].testFunction();
		this.getCompiledTest().getTearDown()();

		this.onTestOK(this.getCompiledTest().getTests()[i]);
	};

	TestRunner.prototype.compile = function() {
		var compiledTest = new CompiledTest();

		var testCase = this.testCase;
		for ( var elementName in testCase) {
			var testElement = testCase[elementName];
			if (typeof testElement == "function") {
				if (elementName == "setUp") {
					compiledTest.setSetUp(testElement);
				} else if (elementName == "tearDown") {
					compiledTest.setTearDown(testElement);
				} else if (elementName == "setUpBeforeClass") {
					compiledTest.setSetUpBeforeClass(testElement);
				} else if (elementName == "getTestCaseName") {
					compiledTest.setTestCaseName(testElement());
				} else if (elementName == "tearDownAfterClass") {
					compiledTest.setTearDownAfterClass(testElement);
				} else if (elementName.indexOf("test") == 0) {
					compiledTest.addTest(elementName, testElement);
				}
			}
		}

		return compiledTest;
	};

	TestRunner.prototype.getCompiledTest = function() {
		if (this.compiledTest == null) {
			this.compiledTest = this.compile();
		}

		return this.compiledTest;
	};

	TestRunner.prototype.onAssertException = function(assertException, test) {
		if (this.testRenderer.onTestFail) {
			this.testRenderer.onTestFail(test.name, assertException.msg, assertException.customMessage, test.funct);
		}
	};

	TestRunner.prototype.onTestError = function(error, test) {
		if (this.testRenderer.onTestError) {
			this.testRenderer.onTestError(test.name, error, test.funct);
		}
	};

	TestRunner.prototype.onTestOK = function(test) {
		if (this.testRenderer.onTestOK) {
			this.testRenderer.onTestOK(test.name, test.funct);
		}
	};

	TestRunner.prototype.onCallTest = function(test) {
		if (this.testRenderer.onCallTest) {
			this.testRenderer.onCallTest(test.name, test.funct);
		}
	};

	TestRunner.prototype.onStartTests = function() {
		if (this.testRenderer.onStartTests) {
			this.testRenderer.onStartTests(this.getCompiledTest().getTestCaseName());
		}
	};

	/**
	 * AsynchTestRunner
	 */
	function AsynchTestRunner(testCase, testRenderer) {
		this.init(testCase, testRenderer);
	}

	AsynchTestRunner.prototype = new TestRunner();

	AsynchTestRunner.prototype.callTest = function(i) {
		var testElement = this.getCompiledTest().getTests()[i];

		this.onCallTest(testElement);

		this.getCompiledTest().getSetUp()();
		testElement.testFunction(new AsynchAssert(this, testElement), new TestManager(this, testElement));
		this.getCompiledTest().getTearDown()();
	};

	/**
	 * TestManager for Asynch Tests
	 */
	function TestManager(asynchTestRunner, testElement) {
		this.asynchTestRunner = asynchTestRunner;
		this.testElement = testElement;
	}

	TestManager.prototype.finishTest = function() {
		if (this.timer != null) {
			clearTimeout(this.timer);
		}

		if (this.testElement.testFailed == null || !this.testElement.testFailed) {
			this.asynchTestRunner.onTestOK(this.testElement);
		}
	};

	TestManager.prototype.setTimeOut = function(delay) {
		var _this = this;
		this.timer = setTimeout(function() {
			_this.testElement.testFailed = true;
			_this.asynchTestRunner.onAssertException(new AssertException("Timed out."), _this.testElement);
		}, delay);
	};

	/**
	 * DefaultTestRenderer
	 */
	function DefaultTestRenderer() {
		var ID = "testResult";
		this.resultContainer = window.document.getElementById(ID);

		if (this.resultContainer == null) {
			this.resultContainer = document.createElement('div');
			this.resultContainer.setAttribute('id', ID);
			document.body.appendChild(this.resultContainer);
		}

		this.tests = {};
	}

	DefaultTestRenderer.prototype.createResultTable = function(testCaseName) {
		var table = document.createElement("table");
		table.className = "testResultTable";
		this.tableHeader = this.createResultTableHeader(testCaseName);
		this.tableBody = this.createResultTableBody();

		table.appendChild(this.tableHeader);
		table.appendChild(this.tableBody);

		return table;
	};

	DefaultTestRenderer.prototype.createResultTableHeader = function(testCaseName) {
		var thead = document.createElement("thead");

		if (testCaseName != null) {
			var titleTr = document.createElement("tr");
			titleTr.className = "testCaseName";

			var titleTh = document.createElement("th");
			titleTh.setAttribute("colSpan", 4);
			titleTh.innerHTML = testCaseName;
			titleTr.appendChild(titleTh);

			thead.appendChild(titleTr);
		}

		var tr = document.createElement("tr");

		var thTestName = document.createElement("th");
		thTestName.innerHTML = "Test";

		var thTestResult = document.createElement("th");
		thTestResult.innerHTML = "Result";

		var thErrorMessage = document.createElement("th");
		thErrorMessage.innerHTML = "Error";

		var thCustomMessage = document.createElement("th");
		thCustomMessage.innerHTML = "Custom Message";

		tr.appendChild(thTestName);
		tr.appendChild(thTestResult);
		tr.appendChild(thErrorMessage);
		tr.appendChild(thCustomMessage);

		thead.appendChild(tr);

		return thead;
	};

	DefaultTestRenderer.prototype.createResultTableBody = function() {
		return document.createElement("tbody");
	};

	DefaultTestRenderer.prototype.updateResultTableEntry = function(testName, testPassed, failMessage, customMessage) {
		var testEntry = this.tests[testName];

		if (testPassed) {
			testEntry.resultTd.className += " testResultOK";
			testEntry.resultTd.innerHTML = "OK";
		} else {
			testEntry.resultTd.className += " testResultFail";
			testEntry.resultTd.innerHTML = "Fail";
		}

		if (failMessage != null) {
			testEntry.failMessageTd.innerHTML = failMessage;
		}

		if (customMessage != null) {
			testEntry.failCustomMessageTd.innerHTML = customMessage;
		}
	};

	DefaultTestRenderer.prototype.createResultTableEntry = function(testName) {
		var tr = document.createElement('tr');

		tr.appendChild(this.createTestNameTd(testName));

		var resultTd = this.createResultTd();
		tr.appendChild(resultTd);

		var failMessageTd = this.createTestFailMessageTd();
		tr.appendChild(failMessageTd);

		var failCustomMessageTd = this.createTestFailCustomMessageTd();
		tr.appendChild(failCustomMessageTd);

		return {
			"tr" : tr,
			"resultTd" : resultTd,
			"failMessageTd" : failMessageTd,
			"failCustomMessageTd" : failCustomMessageTd,
		};
	};

	DefaultTestRenderer.prototype.createTestEntryTd = function(innerHTML) {
		var testNameTd = document.createElement('td');
		if (innerHTML != null) {
			testNameTd.innerHTML = innerHTML;
		}

		return testNameTd;
	};

	DefaultTestRenderer.prototype.createTestNameTd = function(testName) {
		var td = this.createTestEntryTd(testName);
		td.className = "testName";

		return td;
	};

	DefaultTestRenderer.prototype.createResultTd = function() {
		var td = this.createTestEntryTd();
		td.className = "testResult";

		return td;
	};

	DefaultTestRenderer.prototype.createTestFailMessageTd = function() {
		var td = this.createTestEntryTd();
		td.className = "testFailMessage";

		return td;
	};

	DefaultTestRenderer.prototype.createTestFailCustomMessageTd = function() {
		var td = this.createTestEntryTd();
		td.className = "testFailCustomMessage";

		return td;
	};

	DefaultTestRenderer.prototype.onStartTests = function(testCaseName) {
		this.resultTable = this.createResultTable(testCaseName);
		this.resultContainer.appendChild(this.resultTable);
	};

	DefaultTestRenderer.prototype.onTestError = function(testName, error) {
		this.updateResultTableEntry(testName, false, "Unexpected Error.", error);
	};

	DefaultTestRenderer.prototype.onTestFail = function(testName, failMsg, customMsg) {
		this.updateResultTableEntry(testName, false, failMsg, customMsg);
	};

	DefaultTestRenderer.prototype.onTestOK = function(testName) {
		this.updateResultTableEntry(testName, true);
	};

	DefaultTestRenderer.prototype.onCallTest = function(testName) {
		var okEntry = this.createResultTableEntry(testName, true);
		this.tests[testName] = okEntry;
		this.tableBody.appendChild(okEntry.tr);
	};

	/**
	 * Compiled test for TestRunner
	 */
	function CompiledTest() {
		this.setUp = function() {
		};

		this.tearDown = function() {
		};

		this.setUpBeforeClass = function() {
		};

		this.tearDownAfterClass = function() {

		};

		this.tests = [];
	}

	CompiledTest.prototype.setSetUp = function(setUp) {
		this.setUp = setUp;
	};

	CompiledTest.prototype.getSetUp = function() {
		return this.setUp;
	};

	CompiledTest.prototype.setTearDown = function(tearDown) {
		this.tearDown = tearDown;
	};

	CompiledTest.prototype.getTearDown = function() {
		return this.tearDown;
	};

	CompiledTest.prototype.setSetUpBeforeClass = function(setUpBeforeClass) {
		this.setUpBeforeClass = setUpBeforeClass;
	};

	CompiledTest.prototype.getSetUpBeforeClass = function() {
		return this.setUpBeforeClass;
	};

	CompiledTest.prototype.setTearDownAfterClass = function(tearDownAfterClass) {
		this.tearDownAfterClass = tearDownAfterClass;
	};

	CompiledTest.prototype.getTearDownAfterClass = function() {
		return this.tearDownAfterClass;
	};

	CompiledTest.prototype.getTestCaseName = function() {
		return this.testCaseName;
	};

	CompiledTest.prototype.setTestCaseName = function(testCaseName) {
		this.testCaseName = testCaseName;
	};

	CompiledTest.prototype.addTest = function(name, test) {
		this.tests.push({
			"name" : name,
			"testFunction" : test
		});
	};

	CompiledTest.prototype.getTests = function() {
		return this.tests;
	};

	/**
	 * Assert Exception
	 */
	function AssertException(msg, customMessage) {
		this.msg = msg;
		this.customMessage = customMessage;
	}

	AssertException.prototype.toString = function() {
		return "AssertException " + JSON.stringify(this);
	};

	/**
	 * Assert utilities for Assert class
	 */
	AssertUtils = {};
	AssertUtils.createDiffMessage = function(expected, actual) {
		return "expected '" + expected + "', but was '" + actual + "'";
	};
	AssertUtils.checkValid = function(valid, expected, actual, customMessage) {
		if (!valid) {
			errorMsg = this.createDiffMessage(expected, actual);
			throw new AssertException(errorMsg, customMessage);
		}
	};

	/**
	 * Assert test utility
	 */
	Assert = {};
	Assert.assertEquals = function(expected, actual, customMessage) {
		AssertUtils.checkValid(expected == actual, expected, actual, customMessage);
	};

	Assert.assertSame = function(expected, actual, customMessage) {
		AssertUtils.checkValid(expected === actual, expected, actual, customMessage);
	};

	Assert.assertNotSame = function(expected, actual, customMessage) {
		AssertUtils.checkValid(expected !== actual, expected, actual, customMessage);
	};

	Assert.fail = function(customMessage) {
		throw new AssertException("fail", customMessage);
	};

	Assert.assertNotNull = function(actual, customMessage) {
		AssertUtils.checkValid(actual != null, "not null", "null", customMessage);
	};

	Assert.assertNull = function(actual, customMessage) {
		AssertUtils.checkValid(actual == null, "null", actual, customMessage);
	};

	Assert.assertTrue = function(actual, customMessage) {
		AssertUtils.checkValid(actual == true, "true", actual, customMessage);
	};

	Assert.assertFalse = function(actual, customMessage) {
		AssertUtils.checkValid(actual == false, "false", actual, customMessage);
	};

	Assert.assertArrayEquals = function(expected, actual, customMessage) {
		if (expected.length != actual.length) {
			throw new AssertException("Expected len: " + expected.length + " but was " + actual, customMessage);
		}

		for ( var i = 0; i < expected.length; i++) {
			AssertUtils.checkValid(expected[i] == actual[i], expected[i], actual[i], customMessage);
		}
	};

	Assert.staticImport = function() {
		for ( var key in Assert) {
			if (key.indexOf("assert") == 0) {
				window[key] = Assert[key];
			}
		}

		window['fail'] = Assert.fail;
	};

	function AsynchAssert(testRunner, testElement) {
		this.testRunner = testRunner;
		this.testElement = testElement;
	}

	AsynchAssert.callAssertFunct = function(assertFunct) {
		try {
			assertFunct();
		} catch (assertException) {
			this.testRunner.onAssertException(assertException, this.testElement);
		}
	};

	AsynchAssert.assertEquals = function(expected, actual, customMessage) {
		this.callAssertFunct(function() {
			Assert.assertEquals(expected, actual, customMessage);
		});
	};

	AsynchAssert.assertSame = function(expected, actual, customMessage) {
		this.callAssertFunct(function() {
			AssertUtils.assertSame(expected, actual, customMessage);
		});
	};

	AsynchAssert.assertNotSame = function(expected, actual, customMessage) {
		this.callAssertFunct(function() {
			AssertUtils.assertNotSame(expected, actual, customMessage);
		});
	};

	AsynchAssert.fail = function(customMessage) {
		this.callAssertFunct(function() {
			AssertUtils.fail(ecustomMessage);
		});
	};

	AsynchAssert.assertNotNull = function(actual, customMessage) {
		this.callAssertFunct(function() {
			AssertUtils.assertNotNull(actual, customMessage);
		});
	};

	AsynchAssert.assertNull = function(actual, customMessage) {
		this.callAssertFunct(function() {
			AssertUtils.assertNull(actual, customMessage);
		});
	};

	AsynchAssert.assertTrue = function(actual, customMessage) {
		this.callAssertFunct(function() {
			AssertUtils.assertTrue(actual, customMessage);
		});
	};

	AsynchAssert.assertFalse = function(actual, customMessage) {
		this.callAssertFunct(function() {
			AssertUtils.assertFalse(actual, customMessage);
		});
	};

	window.TestSuite = TestSuite;
	window.TestRunner = TestRunner;
	window.AsynchTestRunner = AsynchTestRunner;
	window.Assert = Assert;
}());